package com.cloud.utils;

import com.cloud.common.domain.map.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 点聚合算法 (百度坐标系)
 * 参考：https://blog.csdn.net/ylforever/article/details/88712830
 *
 * @author yzj
 */
public class GisGroupUtils<T extends GroupModel> {

    // 汇聚算法参数
    private ClusterOption option = new ClusterOption();

    // K-D树
    private List<JKDBush> trees = Arrays.asList(new JKDBush[option.getMaxZoom() + 2]);

    // 参数汇聚的点数据列表
    private List<T> points = null;

    public GisGroupUtils() {

    }

    public void setClusterOption(ClusterOption option) {
        this.option = option;
        this.trees = Arrays.asList(new JKDBush[option.getMaxZoom() + 2]);
    }

    /**
     * 加载数据,生成KD树。
     *
     * @param points
     */
    public void load(List<T> points) {
        this.points = points;
        // 构建聚合点数据
        List<JKDNode> clusters = new ArrayList<>();
        for (int i = 0; i < this.points.size(); i++) {
            if (points.get(i) == null) {
                continue;
            }
            clusters.add(createPointCluster(points.get(i), i));
        }
        trees.set(option.getMaxZoom() + 1, new JKDBush(clusters));
        // 逐层构建每一级的汇聚节点
        for (int zoom = option.getMaxZoom(); zoom >= option.getMinZoom(); zoom--) {
            List<JKDNode> clustersList = buildCluster(clusters, zoom);
            trees.set(zoom, new JKDBush(clustersList));
            clusters = clustersList;
        }
    }

    /**
     * 加载G84坐标并转换百度数据,生成KD树。
     *
     * @param points
     */
    public void loadG84ToBd09Point(List<T> points) {
        this.points = points;
        // 构建聚合点数据
        List<JKDNode> clusters = new ArrayList<>();
        for (int i = 0; i < this.points.size(); i++) {
            if (points.get(i) == null) {
                continue;
            }
            T point = points.get(i);
            double[] bd09Point = GPSUtils.Gcj02_To_Bd09(point.getLongitude(), point.getLatitude());
            point.setLongitude(bd09Point[0]);
            point.setLatitude(bd09Point[1]);
            clusters.add(createPointCluster(point, i));
        }
        trees.set(option.getMaxZoom() + 1, new JKDBush(clusters));
        // 逐层构建每一级的汇聚节点
        for (int zoom = option.getMaxZoom(); zoom >= option.getMinZoom(); zoom--) {
            List<JKDNode> clustersList = buildCluster(clusters, zoom);
            trees.set(zoom, new JKDBush(clustersList));
            clusters = clustersList;
        }
    }

    /**
     * 创建点集群
     *
     * @param point
     * @param id
     * @return
     */
    private JKDNode createPointCluster(T point, int id) {
        JKDNode node = new JKDNode();
        node.setId(id);
        node.setX(lngX(point.getLongitude()));
        node.setY(latY(point.getLatitude()));
        node.setParentId(-1);
        node.setZoom(Integer.MAX_VALUE);
        node.setOrignalId(point.getId());
        node.setIndex(id);
        return node;
    }

    /**
     * 获取聚簇对象。
     *
     * @param bbox 当前屏幕的边界(左下角、右上角坐标)
     * @param zoom 当前地图的放大级别
     * @return
     */
    public List<ClusterNode<T>> getClusters(List<Double> bbox, int zoom) {
        double minLng = ((bbox.get(0) + 180) % 360 + 360) % 360 - 180;
        double minLat = Math.max(-90, Math.min(90, bbox.get(1)));
        double maxLng = bbox.get(2) == 180 ? 180 : ((bbox.get(2) + 180) % 360 + 360) % 360 - 180;
        double maxLat = Math.max(-90, Math.min(90, bbox.get(3)));
        if (bbox.get(2) - bbox.get(0) >= 360) {
            minLng = -180;
            maxLng = 180;
        } else if (minLng > maxLng) {
            List<Double> easternBBox = new ArrayList<>();
            easternBBox.add(minLng);
            easternBBox.add(minLat);
            easternBBox.add(180.0);
            easternBBox.add(maxLat);
            List<ClusterNode<T>> easternHem = getClusters(easternBBox, zoom);
            List<Double> westernBBox = new ArrayList<>();
            westernBBox.add(-180.0);
            westernBBox.add(minLat);
            westernBBox.add(maxLng);
            westernBBox.add(maxLat);
            List<ClusterNode<T>> westernHem = getClusters(westernBBox, zoom);
            easternHem.addAll(westernHem);
            return easternHem;
        }
        JKDBush tree = trees.get(limitZoom(zoom));
        List<Integer> ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat));
        List<ClusterNode<T>> clusters = new ArrayList<>();
        for (int id : ids) {
            JKDNode node = tree.getPoints().get(id);
            setClusterInfo(id, node, clusters);
        }
        // 聚合数据地图层级超出最大聚合范围则查询所有子节点位
        if (zoom >= option.getMaxClusterZoom()) {
            List<ClusterNode<T>> leavesList = new ArrayList<>();
            for (ClusterNode<T> clusterNode : clusters) {
                leavesList.addAll(getLeaves(leavesList, clusterNode));
            }
            if (leavesList.size() > 0) {
                clusters.addAll(leavesList);
            }
        }
        return clusters;
    }

    /**
     * 获取聚簇节点下所有叶子节点
     *
     * @param clusterNode
     * @return
     */
    public List<ClusterNode<T>> getLeaves(List<ClusterNode<T>> leavesList, ClusterNode<T> clusterNode) {
        int limit = Integer.MAX_VALUE;
        int offset = 0;
        appendLeaves(leavesList, clusterNode, limit, offset, 0);
        return leavesList;
    }

    /**
     * 构建聚簇对象。
     *
     * @param points
     * @param zoom
     * @return
     */
    private List<JKDNode> buildCluster(List<JKDNode> points, int zoom) {
        List<JKDNode> clusters = new ArrayList<>();
        double radius = option.getRadius() / (option.getExtent() * Math.pow(2, zoom));
        for (int i = 0; i < points.size(); i++) {
            JKDNode node = points.get(i);
            if (node.getZoom() <= zoom) {
                continue;
            }
            node.setZoom(zoom);
            // 找到所有临近的节点做汇聚
            JKDBush tree = trees.get(zoom + 1);
            List<Integer> neighborIds = tree.within(node.getX(), node.getY(), radius);
            int numPoints = (node.getNumPoints() != 0) ? node.getNumPoints() : 1;
            double wx = node.getX() * numPoints;
            double wy = node.getY() * numPoints;
            // cluster id中包含的zoom和原始对象ID的信息
            int id = (i << 5) + (zoom + 1);
            for (int neighborId : neighborIds) {
                JKDNode node1 = tree.getPoints().get(neighborId);
                // 过滤掉已处理过的邻居节点
                if (node1.getZoom() <= zoom) {
                    continue;
                }
                node1.setZoom(zoom);
                int numPoints2 = (node1.getNumPoints() != 0) ? node1.getNumPoints() : 1;
                wx += node1.getX() * numPoints2;
                wy += node1.getY() * numPoints2;
                numPoints += numPoints2;
                node1.setParentId(id);
            }
            if (numPoints == 1) {
                clusters.add(node);
            } else {
                node.setParentId(id);
                clusters.add(createCluster(wx / numPoints, wy / numPoints, id, numPoints, null));
            }
        }
        return clusters;
    }

    /**
     * 获取聚簇节点下的子节点。
     *
     * @param kdNodeId
     * @return
     */
    private List<ClusterNode<T>> getChildren(int kdNodeId) {
        int originId = kdNodeId >> 5;
        int originZoom = kdNodeId % 32;
        List<ClusterNode<T>> children = new ArrayList<>();
        if (option.getMinZoom() >= originZoom || option.getMaxZoom() <= originZoom) {
            return children;
        }
        JKDBush index = trees.get(originZoom);
        if (index == null) {
            return children;
        }
        JKDNode origin = index.getPoints().get(originId);
        if (origin == null) {
            return children;
        }
        double radius = option.getRadius() / (option.getExtent() * Math.pow(2, originZoom - 1));
        List<Integer> ids = index.within(origin.getX(), origin.getY(), radius);
        for (int id : ids) {
            JKDNode node = index.getPoints().get(id);
            if (node.getParentId() == kdNodeId) {
                setClusterInfo(id, node, children);
            }
        }
        return children;
    }

    /**
     * 设置聚合对象信息
     */
    private void setClusterInfo(int id, JKDNode node, List<ClusterNode<T>> clusters) {
        if (node.getNumPoints() > 0) {
            T vo = points.get(id);
            ClusterNode<T> cn = new ClusterNode<>();
            cn.setCluster(true);
            cn.setKdNodeId(node.getId());
            cn.setClusterId(vo.getId());
            cn.setPointCount(node.getNumPoints());
//            cn.setX(xLng(node.getX()));
//            cn.setY(yLat(node.getY()));
            cn.setX(vo.getLongitude());
            cn.setY(vo.getLatitude());
            cn.setData(vo);
            clusters.add(cn);
        } else {
            T vo = points.get(node.getIndex());
            ClusterNode<T> cn = new ClusterNode<>();
            cn.setClusterId(vo.getId());
            cn.setX(vo.getLongitude());
            cn.setY(vo.getLatitude());
            cn.setData(vo);
            clusters.add(cn);
        }
    }

    /**
     * 添加叶子节点
     *
     * @param result
     * @param clusterNode
     * @param limit
     * @param offset
     * @param skipped
     * @return
     */
    private int appendLeaves(List<ClusterNode<T>> result, ClusterNode<T> clusterNode, int limit, int offset, int skipped) {
        List<ClusterNode<T>> children = getChildren(clusterNode.getKdNodeId());
        for (ClusterNode<T> child : children) {
            if (child.isCluster()) {
                if (skipped + child.getPointCount() <= offset) {
                    // 跳过整个聚簇节点
                    skipped += child.getPointCount();
                } else {
                    skipped = appendLeaves(result, child, limit, offset, skipped);
                }
            } else if (skipped < offset) {
                skipped++;
            } else {
                if (clusterNode.getClusterId() != child.getClusterId()) {
                    result.add(child);
                }
            }
            if (result.size() == limit) {
                break;
            }
        }
        return skipped;
    }

    private int limitZoom(int z) {
        return Math.max(option.getMinZoom(), Math.min(z, option.getMaxZoom() + 1));
    }

    /**
     * 将经度转成墨卡托坐标
     *
     * @param lng
     * @return
     */
    private double lngX(double lng) {
        return lng / 360 + 0.5;
    }

    private double latY(double lat) {
        double sin = Math.sin(lat * Math.PI / 180);
        double y = (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI);
        return y < 0 ? 0 : y > 1 ? 1 : y;
    }

    /**
     * 墨卡托坐标转经度。
     *
     * @return
     */
    private double xLng(double x) {
        return (x - 0.5) * 360;
    }

    private double yLat(double y) {
        double y2 = (180 - y * 360) * Math.PI / 180;
        return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
    }

    /**
     * 构建聚合节点。
     *
     * @return
     */
    private JKDNode createCluster(double x, double y, int id, int numPoints, Object properties) {
        JKDNode node = new JKDNode();
        node.setId(id);
        node.setX(x);
        node.setY(y);
        node.setParentId(-1);
        node.setNumPoints(numPoints);
        node.setProperties(properties);
        return node;
    }

    /**
     * 设置盒子坐标赋值
     *
     * @param mapParam
     * @return
     */
    public List<Double> setBoxDefault(MapParam mapParam) {
        List<Double> bbox = new ArrayList<>();
        bbox.add(mapParam.getBottomLeftLongitude());
        bbox.add(mapParam.getBottomLeftLatitude());
        bbox.add(mapParam.getTopRightLongitude());
        bbox.add(mapParam.getTopRightLatitude());
        return bbox;
    }

    /**
     * 设置盒子坐标转G84坐标
     *
     * @return
     */
    public List<Double> setBoxAxisToG84(MapParam mapParam) {
        List<Double> bbox = new ArrayList<>();
        double[] bottomLeft = GPSUtils
                .bd09_To_gps84(mapParam.getBottomLeftLongitude(), mapParam.getBottomLeftLatitude());
        bbox.add(bottomLeft[0]);
        bbox.add(bottomLeft[1]);
        double[] topRight = GPSUtils
                .bd09_To_gps84(mapParam.getTopRightLongitude(), mapParam.getTopRightLatitude());
        bbox.add(topRight[0]);
        bbox.add(topRight[1]);
        return bbox;
    }

}
